热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux|多线程与fork

经由fork()创建的子进程,其中只有一个线程。子进程里仅存的线程,对应着主进程里调用fork()函数的线程。此外,主进程的整个虚存空间都

经由 fork() 创建的子进程,其中只有一个线程。子进程里仅存的线程,对应着主进程里调用 fork() 函数的线程。此外,主进程的整个虚存空间都被复制到了子进程。因而,包括互斥锁、条件变量、其余线程的内部对象等,都保持着原样。由此引发的问题,可以考虑用 pthread_atfork() 函数解决。
参考自:https://liam.page/2017/01/17/fork-safe/

1.线程内fork产生的子进程,只会执行该线程部分(主进程不会向下执行)

#include
#include
#include
#include
#include
#include
/* 功能说明:* 主函数 ---- ------main:主线程---> 打印:main:主线程id=..* 工作线程 --fork() ---fun:父进程---> 打印: fun:父进程id=.. * |---fun:子进程---> 打印: fun: 子进程id=..*/void *Thread_fun()
{pid_t pid &#61; fork();if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}}else{for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}}
}int main()
{pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);exit(0);
}

在这里插入图片描述
fork产生的子进程中只有一个线程。

执行结果&#xff1a;
main::主线程 【1】:id &#61; 74226
fun::父进程 【1】:id &#61; 74226
fun::子进程 【1】:id &#61; 74228
fun::父进程 【2】:id &#61; 74226
main::主线程 【2】:id &#61; 74226
fun::子进程 【2】:id &#61; 74228
fun::父进程 【3】:id &#61; 74226
main::主线程 【3】:id &#61; 74226
fun::子进程 【3】:id &#61; 74228
main is stop …

2.尝试加锁控制执行状态

如果我们把输出屏幕看成一块共享资源的话&#xff0c;我们想要输出结果为&#xff0c;先让main中的打印完成&#xff0c;在打印线程内的。则我们需要加锁完成。

2.1 fork复制了main中锁状态&#xff0c;则线程内的子进程部分加锁。效果不理想

把临界区代码加锁。&#xff08;需要注意的是&#xff0c;fork会复制父进程加锁状态&#xff09;&#xff0c;如果我们代码这样写。

include <stdio.h>
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁
/* 功能说明&#xff1a;* 主函数 ---- ------main:主线程---> 打印&#xff1a;main&#xff1a;主线程id&#61;..* 工作线程 --fork() ---fun&#xff1a;父进程---> 打印&#xff1a; fun&#xff1a;父进程id&#61;.. * |---fun&#xff1a;子进程---> 打印&#xff1a; fun: 子进程id&#61;..** 加锁&#xff1a;保证先让main中的打印先完成*/void *Thread_fun()
{sleep(1); // 保证让主进程先获得锁pid_t pid &#61; fork();if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}else{// 注意&#xff1a;这里没加锁&#xff0c;因为子进程复制的父进程此时是处于加锁状态的&#xff0c;如果这里加锁会产生死锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【f_c%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}}
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证主线程先执行输出操作for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【m %d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);exit(0);
}

执行结果&#xff1a;
main::主线程 【m 1】:id &#61; 75080
main::主线程 【m 2】:id &#61; 75080
fun::子进程 【f_c1】:id &#61; 75082
main::主线程 【m 3】:id &#61; 75080
fun::子进程 【f_c2】:id &#61; 75082
fun::子进程 【f_c3】:id &#61; 75082
main is stop …
fun::父进程 【f_f1】:id &#61; 75080
fun::父进程 【f_f2】:id &#61; 75080
fun::父进程 【f_f3】:id &#61; 75080

可以看到&#xff0c;在父进程中&#xff0c;遵循先答应main的内容&#xff0c;在打印线程函数。&#xff08;main is stop … 之后才执行fun::父进程&#xff09;。但是由于子进程中临界区没有加锁&#xff0c;子进程的输出不受限制&#xff0c;干扰了主进程中的main的输出。

2.1 fork复制了main中锁状态&#xff0c;则线程内的子进程部分加锁。产生死锁&#xff0c;子进程一直阻塞

如果我们给子进程中的临界区加锁又会产生另一种问题。&#xff08;死锁&#xff09;以下代码仅修改了子进程的临界区进行加锁操作。

#include
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁
/* 功能说明&#xff1a;* 主函数 ---- ------main:主线程---> 打印&#xff1a;main&#xff1a;主线程id&#61;..* 工作线程 --fork() ---fun&#xff1a;父进程---> 打印&#xff1a; fun&#xff1a;父进程id&#61;.. * |---fun&#xff1a;子进程---> 打印&#xff1a; fun: 子进程id&#61;..** 在工作线程中fork&#xff0c;产生的子进程&#xff0c;只会执行工作线程内的代码。但子进程会复制父进程的状态&#xff0c;如加锁。*** 加锁&#xff1a;保证先让main中的打印先完成* 问题&#xff1a;fork会复制父进程中的加锁状态&#xff0c;即在工作线程中&#xff0c;子进程已经是处于加锁状态的&#xff0c;再次加锁无法完成&#xff0c;则产生死锁。*/// 注意&#xff1a;以下代码子进程会产生死锁
void *Thread_fun()
{sleep(1); // 保证让主进程先获得锁pid_t pid &#61; fork();if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}else{/* 这里产生死锁&#xff0c;复制的父进程中加锁状态,* 但是线程内使用fork不会再执行父进程中的代码&#xff0c;因此..*/pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【f_c%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源}
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证主线程先执行输出操作for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【m %d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);exit(0);
}

输出结果&#xff1a;
main::主线程 【m 1】:id &#61; 75247
main::主线程 【m 2】:id &#61; 75247
main::主线程 【m 3】:id &#61; 75247
main is stop …
fun::父进程 【f_f1】:id &#61; 75247
fun::父进程 【f_f2】:id &#61; 75247
fun::父进程 【f_f3】:id &#61; 75247

2.3.分析阻塞原因

可以看到子进程中&#xff0c;并没有执行打印操作。使用ps查看&#xff0c;发现有进程还未退出&#xff0c;正是我们的子进程。
在这里插入图片描述
父进程执行过程大致如下&#xff1a;
在这里插入图片描述
子进程中&#xff0c;因为工作线程以外的部分不继续执行了&#xff0c;则工作线程一直处于阻塞状态。
在这里插入图片描述

2.4 对fork加锁&#xff08;试探锁&#xff0c;试探main中是否已经解锁&#xff09;

解决方案&#xff1a;
确保fork()时&#xff0c;父进程是没有加锁的。因为本程序需要实现先打印main的内容&#xff0c;因此我们可以这样做&#x1f447;&#xff0c;确保main中先打印并且不会占用占用锁资源。

// 尝试加锁&#xff0c;如果可以加锁&#xff0c;则证明父进程没有占用锁资源
pid_t pid &#61; fork();
// 解锁

代码如下&#xff1a;

#include
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁void *Thread_fun()
{pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证临界区锁以释放&#xff0c;不会被fork复制到子进程中阻塞临界区pid_t pid &#61; fork();pthread_mutex_unlock(&mutex); // 解锁if(pid !&#61; 0) // 父进程&#xff1a;pid等于子进程id{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::父进程 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}else{pthread_mutex_lock(&mutex); // 加锁for(int i &#61; 0; i < 3; i&#43;&#43;){printf(" fun::子进程 【f_c%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&mutex); // 解锁}
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_t id; // 创建线程并调用工作线程int res &#61; pthread_create(&id,NULL,Thread_fun,NULL);assert(res &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功pthread_mutex_lock(&mutex); // 加锁&#xff0c;保证主线程先执行输出操作for(int i &#61; 0; i < 3; i&#43;&#43;){printf("main::主线程 【m %d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&mutex); // 解锁&#xff0c;释放输出缓冲区资源// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);exit(0);
}

输出结果&#xff1a;
main::主线程 【m 1】:id &#61; 75807
main::主线程 【m 2】:id &#61; 75807
main::主线程 【m 3】:id &#61; 75807
main is stop …
fun::父进程 【f_f1】:id &#61; 75807
fun::子进程 【f_c1】:id &#61; 75811
fun::子进程 【f_c2】:id &#61; 75811
fun::父进程 【f_f2】:id &#61; 75807
fun::子进程 【f_c3】:id &#61; 75811
fun::父进程 【f_f3】:id &#61; 75807

3.系统提供的解决方案 pthread_atfork

如果我们有很多不同的锁&#xff0c;按照我们之前的解决方案&#xff0c;我们需要在 fork 之前进行 n 多次的试探动作&#xff0c;不仅麻烦还极容易引起死锁。而系统为我们提供了一个函数可以解决这个问题。

#include int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));#include
#include
#include
#include
#include
#include pthread_mutex_t mutex; // 创建互斥锁
pthread_mutex_t lock; // 创建互斥锁void prepare() {pthread_mutex_lock(&mutex); // 加锁printf("prepare: 加锁 ...\n");
}void parent() {pthread_mutex_unlock(&mutex);printf("parent : 父进程解锁 ...\n");
}void child() {pthread_mutex_unlock(&mutex);printf("child : 子进程解锁 ...\n");
}void *Thread_fun(void*arg)
{sleep(1);pthread_mutex_lock(&lock); // 加锁for(int i &#61; 0; i < 5; i&#43;&#43;){printf(" fun::%s 【f_f%d】:id &#61; %d\n",(char*)arg,i&#43;1,getpid());sleep(1);}pthread_mutex_unlock(&lock); // 解锁
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_mutex_init(&lock,NULL);pthread_t id; // 创建线程并调用工作线程// 注册forkint res &#61; pthread_atfork(prepare, parent, child);assert(res &#61;&#61; 0);char buff[] &#61; "父进程";int ret &#61; pthread_create(&id,NULL,Thread_fun,(void*)buff);assert(ret &#61;&#61; 0); // 返回值为0&#xff0c;表示创建成功/* fork调用前调用preoarefork调用完&#xff0c;在调用child和parent*/pid_t pid &#61; fork();/*在父进程中&#xff08;当前进程中&#xff09;调用fork。经由 fork() 创建的子进程&#xff0c;其中只有一个线程&#xff08;调用fork的线程&#xff0c;即主线程&#xff09;因此&#xff0c;fork产生的子进程中&#xff0c;不会执行Thread_fun()函数&#xff0c;则我们可以在父进程&#xff08;当前进程中手动调用&#xff09;*/if(pid &#61;&#61; 0) // 子进程{sleep(1); // 保证main中父进程先执行// 注&#xff1a;这里的子进程与父进程是两个进程&#xff0c;不共享锁。同步需要设置管道或者信号量等 // ********strcpy(buff,"子进程");Thread_fun((void*)buff);}else // 父进程{pthread_mutex_lock(&lock); // 加锁for(int i &#61; 0; i < 10; i&#43;&#43;){printf("main::主函数 【f_f%d】:id &#61; %d\n",i&#43;1,getpid());sleep(1);}printf("main is stop ... \n");pthread_mutex_unlock(&lock); // 解锁}// 等待工作线程结束&#xff0c;在此阻塞pthread_join(id,NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);pthread_mutex_destroy(&lock);exit(0);
}

输出&#xff1a;
父进程&#xff1a;先输出 main中的&#xff0c;再输出工作线程中的
子进程&#xff1a;只有一个线程&#xff0c;在main中&#xff0c;通过手动调用执行工作函数。由于进程间全局变量&#xff08;我们定义的锁&#xff09;不共享&#xff0c;所以子进程在输出时不受锁影响。如有需要加入型号量等。。


推荐阅读
  • Linux 中使用 clone 函数来创建线程
    2019独角兽企业重金招聘Python工程师标准Linux上创建线程一般使用的是pthread库实际上libc也给我们提供了创建线程的函数那就是cloneintclone(i ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
  • 作者一直强调的一个概念叫做oneloopperthread,撇开多线程不谈,本篇博文将学习,怎么将传统的IO复用pollepoll封装到C++类中。1.IO复用复习使用p ... [详细]
  • 本文详细介绍了GetModuleFileName函数的用法,该函数可以用于获取当前模块所在的路径,方便进行文件操作和读取配置信息。文章通过示例代码和详细的解释,帮助读者理解和使用该函数。同时,还提供了相关的API函数声明和说明。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 1Lock与ReadWriteLock1.1LockpublicinterfaceLock{voidlock();voidlockInterruptibl ... [详细]
  • 线程漫谈——线程基础
    本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。进程与线程理解线程是至关重要的,每个进程至少有一个线程,进程是线程的容器,线程才是真正的执行体,线程必 ... [详细]
  • 主线:设计窗口类注册窗口类产生窗口显示窗口更新窗口消息循环(将消息路由到窗口中去处理)。APPMODUL.CPP源文件被编译链接进入项目,从APPMOD ... [详细]
  • 不知道你是否还记得之前在进程中的信号处理时,提到过阻塞信号集与未决信号集的概念,如果你已经忘记了,请参考《阻塞信号与未决信号》一文回忆一下 ... [详细]
  • C语言编程gcc怎么生成静态库.a和动态库.so
    这篇文章将为大家详细讲解有关C语言编程gcc怎么生成静态库.a和动态库.so,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章 ... [详细]
  • 我首先提高本程序的权限,然后成功得到服务程序(exe)的读写权限,开辟新的远程内存空间,然后拷贝程序执行的代码,但写入我的dll后,服务程序什么反应也没有。 ... [详细]
author-avatar
手机用户2502885897
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有